﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Threading;

namespace ZSens
{
    class Network
    {
        public enum ZSensPacketHeader
        {
            ZS_DETECT,
            //player number left
            ZS_EXIST,
            ZS_MATCH,
            // id assigned
            ZS_ACK,
            //layout info
            ZS_LAYOUT,
            // id
            // operation
            ZS_DATA, ZS_KICK, ZS_DISCONNECT, ZS_TOGGLE_MAP
        }

        public readonly int port = 42306;

        private Device[] deviceList;
        byte[] dataOut = new byte[1024];
        private int maxNumDevice;
        private UdpClient udpClient;
        private System.Diagnostics.Stopwatch stopWatch;
        public Zsens zsens;

        public Network(int numDevice, Zsens zsens)
        {
            this.maxNumDevice = numDevice;
            this.zsens = zsens;
            stopWatch = new System.Diagnostics.Stopwatch();
            deviceList = new Device[numDevice];
            stopWatch.Start();
            for (int i = 0; i < numDevice; i++)
            {
                deviceList[i] = new Device();
            }
            try
            {
                udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, port));
            }
            catch (Exception e)
            {
                MessageBox.Show("udp bind:" + e.ToString());
                Application.Current.Shutdown();
            }
        }

        public void OnSendCallback(IAsyncResult asyncResult)
        {
            ((UdpClient)asyncResult.AsyncState).EndSend(asyncResult);
        }

        public bool Send(byte[] data, int length, IPEndPoint ep)
        {
            if (length > 0)
            {
                try
                {
                    UdpClient uc = new UdpClient();
                    uc.BeginSend((byte[]) data.Clone(), length, ep, new AsyncCallback(OnSendCallback), uc);
                }
                catch (Exception e)
                {
                    return false;
                }
                return true;
            }
            return false;
        }

        public void Stop()
        {
            udpClient.Close();
        }

        public bool validate(byte[] data, int idx, int length, int hash)
        {
            return (hash == Serializing.hashByteArray(data, idx, idx + length));
        }

        public bool ValidatePacket(byte[] data)
        {
            int hash = 0;
            int idx = Serializing.readFromByteArray(data, 1, ref hash);
            if (idx < 0)
                return false;
            return validate(data, idx, data.Length - idx, hash);
        }

        public int MakeExistPacket(byte[] data)
        {
            data[0] = (byte)ZSensPacketHeader.ZS_EXIST;
            return 1;
        }

        public int MakeAckPacket(byte[] data, int playerId)
        {
            data[0] = (byte)ZSensPacketHeader.ZS_ACK;
            data[5] = (byte)playerId;
            int t = Serializing.hashByteArray(data, 5, 6);
            Serializing.writeIntoByteArray(data, 1, t);
            return 6;
        }

        public int MakeLayoutPacket(byte[] data, int id)
        {
            Layout layout = deviceList[id].player.layout;
            data[0] = (byte)ZSensPacketHeader.ZS_LAYOUT;
            data[5] = (byte)id;
            int end = layout.Serialize(data, 6);
            int t = Serializing.hashByteArray(data, 5, end);
            Serializing.writeIntoByteArray(data, 1, t);
            return end;
        }

        public Boolean updateDeviceStatus()
        {
            for (int i = 0; i < maxNumDevice; i++)
            {
                if (deviceList[i].present)
                {
                    Player player = deviceList[i].player;
                    if (player.lostTime > Player.Timeout)
                    {
                        lock (player.lockDisconnect)
                        {
                            if (player.status == Player.Running)
                            {
                                player.lostTime = 0;
                                player.status = Player.Recycling;
                            }
                        }
                        player.lostTime = 0;
                    }
                    if (player.status == Player.Pending)
                    {
                        int length = MakeLayoutPacket(dataOut, i);
                        if (Send(dataOut, length, deviceList[i].ip) == false)
                            return false;
                        deviceList[i].player.status = Player.Running;
                        deviceList[i].player.mapStatus = true;
                        deviceList[i].player.lastTime = stopWatch.ElapsedMilliseconds;
                    }
                    else if (player.status == Player.Running)
                    {
                        if (player.received == false)
                        {
                            player.lostTime = stopWatch.ElapsedMilliseconds - player.lastTime;
                        }
                        else
                        {
                            player.lastTime = stopWatch.ElapsedMilliseconds;
                            player.lostTime = (long)0;
                            player.received = false;
                        }
                    }
                    else if (deviceList[i].player.status == Player.Destructing)
                    {
                        zsens.RemovePlayer(deviceList[i].player);
                        deviceList[i].Disconnect();
                    }
                }
            }
            return true;
        }

        public void Run()
        {
            IPEndPoint ep = null;

            while (true)
            {
                if (!updateDeviceStatus())
                {
                    return;
                }
                byte[] dataIn = null;
                try
                {
                    if (udpClient.Available > 0)
                        dataIn = udpClient.Receive(ref ep);
                }
                catch
                {
                    return;
                }
                if (dataIn == null || dataIn.Length == 0)
                {
                    
                    Thread.Sleep(10);
                    continue;
                }
                byte deviceIdx = 0;
                int length;

                switch ((ZSensPacketHeader)dataIn[0])
                {
                    case ZSensPacketHeader.ZS_DETECT:
                        length = MakeExistPacket(dataOut);
                        if (Send(dataOut, length, ep) == false)
                            return;
                        break;
                    case ZSensPacketHeader.ZS_MATCH:
                        if (!ValidatePacket(dataIn))
                            break;
                        String deviceName = null;
                        Serializing.readFromByteArray(dataIn, 5, ref deviceName); //[header] [hash][hash][hash][hash] [data starts from index 5]
                        for (int i = 0; i < maxNumDevice; i++)
                        {
                            if (deviceList[i].present == false)
                            {
                                deviceList[i].Enable(deviceName, ep, new Player(deviceName));
                                zsens.AddPlayer(deviceList[i].player);
                                length = MakeAckPacket(dataOut, i);
                                if (Send(dataOut, length, ep) == false)
                                    return;
                                break;
                            }
                        }
                        break;
                    case ZSensPacketHeader.ZS_DISCONNECT:
                        if (!ValidatePacket(dataIn))
                            break;
                        if (Serializing.readFromByteArray(dataIn, 5, ref deviceIdx) < 0)
                            break;
                        if (deviceIdx >= maxNumDevice || !deviceList[deviceIdx].present || !deviceList[deviceIdx].ip.Address.Equals(ep.Address))
                            break;
                        Device device = deviceList[deviceIdx];
                        lock (device.player.lockDisconnect)
                        {
                            if (device.player.status <= Player.Running)
                                device.player.status = Player.Recycling;
                        }
                        break;
                    case ZSensPacketHeader.ZS_DATA:
                        if (!ValidatePacket(dataIn))
                            break;
                        if (Serializing.readFromByteArray(dataIn, 5, ref deviceIdx) < 0)
                            break;
                        if (deviceIdx >= maxNumDevice || !deviceList[deviceIdx].present || !deviceList[deviceIdx].ip.Address.Equals(ep.Address))
                            break;

                        Player player = deviceList[deviceIdx].player;
                        if (player.status != Player.Running)
                            break;
                        player.received = true;
                        Layout layout = player.layout;
                        int idx = 6;
                        foreach (Widget widget in layout.widgets)
                        {
                            idx = widget.Update(dataIn, idx);
                            if (idx > dataIn.Length)
                                break;
                        }
                        break;
                    case ZSensPacketHeader.ZS_TOGGLE_MAP:
                        if (!ValidatePacket(dataIn))
                            break;
                        if (Serializing.readFromByteArray(dataIn, 5, ref deviceIdx) < 0)
                            break;
                        if (deviceIdx >= maxNumDevice || !deviceList[deviceIdx].present || !deviceList[deviceIdx].ip.Address.Equals(ep.Address))
                            break;
                        deviceList[deviceIdx].player.mapStatus = !deviceList[deviceIdx].player.mapStatus;
                        break;
                }
            }
        }
    }
}
